﻿using System;
using System.Collections;
using UnityEngine;
using Object = UnityEngine.Object;

namespace MLog.Helpers {
    public class MonoAsync : MonoBehaviour {
        private static MonoAsync Instance;

        private readonly ArrayList _tasks = new ArrayList();

        private void Update() {
            if (_tasks.Count == 0) {
                lock (Instance) {
                    Instance = null;
                    Destroy(gameObject);
                }
            }
        }

        public static AsyncRunnerBuilder<T> StartAsync<T>(Func<AsyncTask<T>, bool> runnable) {
            return new AsyncRunnerBuilder<T>(runnable);
        }

#if UNITY_5_3_OR_NEWER
        public static AsyncRunnerBuilder<T> StartResourceAsync<T>(ResourceRequest request)
            where T : Object {
            return new AsyncRunnerBuilder<T>(task =>
                {
                    if (request.isDone) {
                        task.Data = (T) request.asset;
                    }

                    return request.isDone;
                }
            );
        }
#endif

        private static void CreateIfNeed() {
            if (Instance == null) {
                lock (typeof(MonoAsync)) {
                    var obj = new GameObject();
                    Instance = obj.AddComponent<MonoAsync>();
                    DontDestroyOnLoad(obj);
                }
            }
        }

        public class AsyncRunnerBuilder<T> {
            private readonly AsyncTask<T> _task;

            public AsyncRunnerBuilder(Func<AsyncTask<T>, bool> runnable) {
                _task          = new AsyncTask<T>();
                _task.Runnable = runnable;
            }

            public void Start(Action<T> callback) {
                CreateIfNeed();
                _task.Callback = callback;
                Instance._tasks.Add(_task);
                Instance.StartCoroutine(new Runner<T>(_task).WaitAndRun());
            }
        }


        private class Runner<T> {
            private static readonly WaitForEndOfFrame YieldFrame = new WaitForEndOfFrame();

            private readonly AsyncTask<T> _task;

            public Runner(AsyncTask<T> task) {
                if (task == null) throw new ArgumentNullException("task");
                this._task = task;
            }

            internal IEnumerator WaitAndRun() {
                var flag = true;
                while (flag) {
                    if (_task.Runnable(_task)) {
                        _task.Callback(_task.Data);
                        flag = false;
                        if (Instance != null) {
                            Instance._tasks.Remove(_task);
                        }
                    }
                    else {
                        yield return YieldFrame;
                    }
                }
            }
        }
    }


    public class AsyncTask<T> {
        public Func<AsyncTask<T>, bool> Runnable { set; get; }
        public Action<T> Callback { set; get; }
        public T Data { set; get; }
    }
}